const http = require('http');
const https = require('https');
const httpModule = require('http');
const zlib = require('zlib');
const { URL } = require('url');

// Use cheerio for HTML parsing. Install with: npm i cheerio
let cheerio;
try {
  cheerio = require('cheerio');
} catch (err) {
  console.error('缺少依赖 cheerio，请先执行: npm i cheerio');
  process.exit(1);
}

const MAOYAN_URL = 'https://maoyan.com/films?showType=3';
const MAOYAN_AJAX_URL = 'https://maoyan.com/ajax/more?offset=0&showType=3';
const MAOYAN_MOBILE_HOT = 'https://i.maoyan.com/api/mmdb/movie/v3/list/hot.json?ct=%E5%8C%97%E4%BA%AC&ci=1&channelId=4';

function fetchHtml(targetUrl, redirectCount = 0, cookieJar = '') {
  return new Promise((resolve, reject) => {
    const urlObj = new URL(targetUrl);
    const client = urlObj.protocol === 'http:' ? httpModule : https;
    const request = client.request(
      {
        hostname: urlObj.hostname,
        path: urlObj.pathname + (urlObj.search || ''),
        protocol: urlObj.protocol,
        method: 'GET',
        headers: {
          'Host': urlObj.hostname,
          'User-Agent':
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
          Accept:
            'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
          'Accept-Language': 'zh-CN,zh;q=0.9',
          Connection: 'keep-alive',
          'Accept-Encoding': 'gzip, deflate, br',
          Referer: 'https://maoyan.com/',
          'Upgrade-Insecure-Requests': '1',
          Cookie: cookieJar,
        },
      },
      (res) => {
        const status = res.statusCode || 0;
        // Handle redirects
        if ([301, 302, 303, 307, 308].includes(status)) {
          const location = res.headers.location;
          // Accumulate cookies
          const setCookies = res.headers['set-cookie'] || [];
          const mergedCookie = [cookieJar, ...setCookies.map((c) => c.split(';')[0])]
            .filter(Boolean)
            .join('; ');
          if (location && redirectCount < 5) {
            const nextUrl = new URL(location, targetUrl).toString();
            res.resume();
            resolve(fetchHtml(nextUrl, redirectCount + 1, mergedCookie));
            return;
          }
        }

        let stream = res;
        const encoding = (res.headers['content-encoding'] || '').toLowerCase();
        if (encoding.includes('gzip')) stream = res.pipe(zlib.createGunzip());
        else if (encoding.includes('deflate')) stream = res.pipe(zlib.createInflate());
        else if (encoding.includes('br') && zlib.createBrotliDecompress) stream = res.pipe(zlib.createBrotliDecompress());

        let data = '';
        stream.on('data', (chunk) => (data += chunk));
        stream.on('end', () => {
          if (status >= 200 && status < 300) {
            // merge any cookies from final response
            const setCookies = res.headers['set-cookie'] || [];
            const mergedCookie = [cookieJar, ...setCookies.map((c) => c.split(';')[0])]
              .filter(Boolean)
              .join('; ');
            resolve({ body: data, cookies: mergedCookie });
          } else {
            reject(new Error(`请求失败: ${status} ${res.statusMessage || ''}`));
          }
        });
      }
    );
    request.on('error', reject);
    request.end();
  });
}

function fetchAjaxJson(cookieJar = '') {
  return new Promise((resolve, reject) => {
    const urlObj = new URL(MAOYAN_AJAX_URL);
    const client = https;
    const req = client.request(
      {
        hostname: urlObj.hostname,
        path: urlObj.pathname + (urlObj.search || ''),
        method: 'GET',
        headers: {
          'Host': urlObj.hostname,
          'User-Agent':
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
          Accept: 'application/json, text/javascript, */*; q=0.01',
          'Accept-Language': 'zh-CN,zh;q=0.9',
          Referer: 'https://maoyan.com/films?showType=3',
          'X-Requested-With': 'XMLHttpRequest',
          Cookie: cookieJar,
        },
      },
      (res) => {
        let data = '';
        res.on('data', (c) => (data += c));
        res.on('end', () => {
          try {
            const json = JSON.parse(data);
            resolve(json);
          } catch (e) {
            reject(e);
          }
        });
      }
    );
    req.on('error', reject);
    req.end();
  });
}

function fetchMobileJson() {
  return new Promise((resolve, reject) => {
    const urlObj = new URL(MAOYAN_MOBILE_HOT);
    const req = https.request(
      {
        hostname: urlObj.hostname,
        path: urlObj.pathname + (urlObj.search || ''),
        method: 'GET',
        headers: {
          'User-Agent':
            'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1',
          Accept: 'application/json',
          Referer: 'https://i.maoyan.com/',
        },
      },
      (res) => {
        let data = '';
        res.on('data', (c) => (data += c));
        res.on('end', () => {
          try {
            const json = JSON.parse(data);
            resolve(json);
          } catch (e) {
            reject(e);
          }
        });
      }
    );
    req.on('error', reject);
    req.end();
  });
}

function parseFilms(html) {
  const $ = cheerio.load(html);
  const films = [];

  // 兼容多种页面结构的选择器
  // 旧结构: .movie-list .movie-item 或 dd[data-val]
  $('.movie-item, dd').each((_, el) => {
    const element = $(el);

    // 电影名
    let name =
      element.find('.movie-title, .name a, .title a, a[data-act="movie-click"]').attr('title') ||
      element.find('.movie-title, .name, .title a, a[data-act="movie-click"]').text().trim();

    // 电影 id（很多在 data-val 或 data-id 中）
    let id = undefined;
    const dataVal = element.attr('data-val') || element.find('a').attr('data-val');
    if (dataVal) {
      try {
        // data-val 形如 {movieId:123456}
        const jsonLike = dataVal
          .replace(/\s/g, '')
          .replace(/([a-zA-Z0-9_]+):/g, '"$1":')
          .replace(/'/g, '"');
        const obj = JSON.parse(jsonLike);
        if (obj.movieId) id = String(obj.movieId);
      } catch (_) {}
    }
    if (!id) {
      const href = element.find('a').attr('href') || '';
      const match = href.match(/\/films\/(\d+)/);
      if (match) id = match[1];
    }

    // 评分（有的页面拆分为 .integer 和 .fraction，有的在 .channel-detail-orange）
    let score = undefined;
    const integer = element.find('.integer').text().trim();
    const fraction = element.find('.fraction').text().trim();
    if (integer) score = `${integer}${fraction}`;
    if (!score) {
      const orange = element.find('.channel-detail-orange, .score').text().trim();
      if (orange) score = orange;
    }
    if (!score || score === '') score = '暂无评分';

    if (name && id) {
      films.push({ movieId: id, movieName: name, movieOrange: score });
    }
  });

  return films;
}

// Fallback: try to parse window.__NUXT__ state to extract film info
function parseFromNuxt(html) {
  try {
    const match = html.match(/window\.__NUXT__\s*=\s*(\{[\s\S]*?\});/);
    if (!match) return [];
    const nuxtState = JSON.parse(match[1]);

    const collected = [];
    const seen = new Set();

    function visit(node) {
      if (!node) return;
      if (Array.isArray(node)) {
        node.forEach(visit);
        return;
      }
      if (typeof node === 'object') {
        const keys = Object.keys(node);
        const id = node.movieId || node.id;
        const name = node.movieName || node.nm || node.name;
        const score = node.sc || node.score || node.movieScore;
        if (id && name) {
          const key = String(id);
          if (!seen.has(key)) {
            collected.push({
              movieId: String(id),
              movieName: String(name),
              movieOrange: score ? String(score) : '暂无评分',
            });
            seen.add(key);
          }
        }
        keys.forEach((k) => visit(node[k]));
      }
    }

    visit(nuxtState);
    return collected;
  } catch (_) {
    return [];
  }
}

function renderHtml(films) {
  const rows = films
    .map(
      (f) =>
        `<tr><td>${f.movieId}</td><td>${f.movieName}</td><td>${f.movieOrange}</td></tr>`
    )
    .join('');
  return `<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>猫眼电影 - 爬虫实验</title>
  <style>
    body{font-family:Segoe UI,Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif;background:#f7f8fa;margin:0;padding:24px;color:#1f2328}
    h1{font-size:20px;margin:0 0 16px}
    table{width:100%;border-collapse:collapse;background:#fff}
    th,td{border:1px solid #e5e6eb;padding:10px 12px;text-align:left}
    th{background:#f2f3f5}
    tr:nth-child(odd){background:#fafafa}
    .tip{margin:12px 0;color:#6b7785;font-size:12px}
  </style>
  </head>
<body>
  <h1>猫眼电影 列表</h1>
  <div class="tip">数据来源: <a href="https://maoyan.com/films" target="_blank" rel="noreferrer">maoyan.com/films</a> · 仅用于学习演示</div>
  <table>
    <thead>
      <tr><th>电影id</th><th>电影名称</th><th>电影评分</th></tr>
    </thead>
    <tbody>${rows}</tbody>
  </table>
</body>
</html>`;
}

const server = http.createServer(async (req, res) => {
  if (req.method !== 'GET') {
    res.writeHead(405, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end('仅支持 GET');
    return;
  }

  try {
    const { body: html, cookies } = await fetchHtml(MAOYAN_URL);
    let films = parseFilms(html);
    if (!films || films.length === 0) films = parseFromNuxt(html);
    // Ajax fallback if still empty
    if (!films || films.length === 0) {
      try {
        const ajax = await fetchAjaxJson(cookies);
        const list = (ajax && (ajax.movieList || ajax.list || ajax.data || ajax)) || [];
        const mapped = Array.isArray(list) ? list : [];
        films = mapped
          .map((m) => ({
            movieId: String(m.id || m.movieId || ''),
            movieName: String(m.nm || m.movieName || m.name || ''),
            movieOrange: String(m.sc || m.score || m.movieScore || '暂无评分'),
          }))
          .filter((m) => m.movieId && m.movieName);
      } catch (_) {}
    }
    // Mobile API fallback
    if (!films || films.length === 0) {
      try {
        const mobile = await fetchMobileJson();
        const list = mobile && mobile.data && mobile.data.hot ? mobile.data.hot : [];
        films = list
          .map((m) => ({
            movieId: String(m.id),
            movieName: String(m.nm || m.name),
            movieOrange: m.sc ? String(m.sc) : '暂无评分',
          }))
          .filter((m) => m.movieId && m.movieName);
      } catch (_) {}
    }
    console.log('解析影片数量:', Array.isArray(films) ? films.length : 0);
    const page = renderHtml(films);
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.end(page);
  } catch (err) {
    res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(`抓取失败: ${err.message}`);
  }
});

const PORT = 8081;
server.listen(PORT, () => {
  console.log(`服务器已启动: http://localhost:${PORT}`);
});


